#!/usr/local/bin/ruby
##!ruby
require "cgi"
require 'nkf'
require "MyUtil"
require "LivedoorReaderWp"
require "ErbMan"
 

# todo:ユーザー名の毒抜きしないと
# todo:erb とか
# todo:複数アカウント対応(データファイルが現状ダメ)
class MobileReader

  def initialize
    # せっていせっていせっていせっていせっていせっていせっていせってい
    # リンク先を、携帯変換 URL に置換する。
    # "gmobile" or "hatena" or ""
    # ちなみに、これ使わないとリファラでユーザー名バレ
    @USE_LINK_REPLACE = "hatena"
    # インライン表示画像を、携帯変換 URL に置換する。
    # "mobapic" or "hatena" or ""
    @USE_INLINE_IMAGE_REPLACE = "hatena"
    # "noimage" にしておくと、画像を "image" と言うリンクに置換する。
    # @USE_INLINE_IMAGE_REPLACE の影響も同時に受けます。
    # "noimage" or ""
    @USE_INLINE_IMAGE_REPLACE2 = ""
    # フィード画面での表示件数上限 TITLE_MODE = true の場合は
    # TITLE_MODE_LIMIT の値です。
    # 0 にするとダメです。(おい)
    @LIMIT = 4
    # フィード一覧の一回ごとの取得上限
    # 帯域が広かったり、CGI タイムアウトの時間が長いサーバーでは大きい
    # 値にすると早くなる。
    # 小さい値なら、帯域が狭くタイムアウトが出やすい環境にも適応できる
    # が、リダイレクト限界が小さいブラウザで使えなくなる可能性。
    @SUBS_LIMIT = 300

    # フィード表示画面で本文を表示せず、見出しのリンクだけにします
    @TITLE_MODE = false

    # リンク一覧系画面の表示上限です。nil で無制限
    @LINK_LIST_LIMIT = 9

    # フィード表示画面を開いた瞬間に既読にする
    @YONDARA_KIDOKU = true

    # 外部リンクをリファラカット用の別 CGI 経由に書きかえる
    # ※ リファラ漏れを防止しますが、画像のインライン表示で無変換の場
    # 合はそっちから漏れます。
    # あるいは、画像変換サービスが御丁寧にリファラもそっくり先方に送っ
    # ている可能性もある訳で完全には防げません。
    @USE_IMENU = true

    # gzip 圧縮転送に対応するか否か。
    # true, false 共に、クライアントへの転送と共に CGI, LDR 間の転送にも影響します
    # gzip に対応していないクライアントと言うよりは、zlib の導入出来無い環境を想定してます。
    @USE_GZIP=true

    @SCRIPT_URL = "./" + "MobileReader.cgi"
    @SCR = {
      :init => 1,
      :login => 2,
      :folder => 3,
      :list => 4,
      :load_subs => 5,
      :load_folder => 6,
      :main => 7,
      :rate => 8,
      :view => 9,
      :reload_yaml => 10,
      :touch_all => 11,
      :subscribers => 12,
      :modified_on => 13,
      :history => 14
    }
    @cgi = CGI.new("html3")
  end
  attr_accessor :my_erb

  def get_subs
    unless @subs
      @subs = MyUtil.load_obj(@SUBS_FILE)
    else
      @subs
    end
  end

  def get_folders
    unless @folders
      @folders = MyUtil.load_obj(@FOLDERS_FILE)
    else
      @folders
    end
  end

  def get_history
    unless @history
      begin
        @history = MyUtil.load_obj(@HISTORY_FILE)
      rescue
        []
      end
    else
      @history
    end
  end

  def url(scr, *ary)
    tmp = @SCRIPT_URL + "?" + "&username="+ @USERNAME + "&scr=" + scr.to_s
    ary.each { |v| tmp << "&" + v}
    return tmp
  end

  def redirect(scr, *opt)
    headers={
      'status'=>'REDIRECT',
      "location" => url(scr, *opt)
    }
    cgiout( headers ) {""}
  end

  def login(msg="")
    buf = ErbMan.new.erb(:login).result(binding)
    cgiout(headers) {buf + msg}
  end

  def login_redirect(password)
    # GET で飛んで来るので、何かの拍子に user,pass 漏れると困るので、一度別の URL に飛ばす
    # POST にしないのは、ブックマーク出来るようにする為
    # # クッキー保存してるから、途中のページをブックマーク出来るけど、有効期間がわからんので…
    @l.login(password)
    redirect( @SCR[:load_subs])
  end

  def reload_yaml()
    Dir.glob("*.dump") do |item|
      File.delete(item)
    end
    redirect( @SCR[:load_subs])
  end

  def load_folder
    folders = @l.folders["name2id"]
    MyUtil.save_obj(folders, @FOLDERS_FILE)
    redirect(@SCR[:main])
  end

  def load_subs(from_id=0)
    subs = @l.subs(1,from_id,@SUBS_LIMIT)
    MyUtil.save_obj(subs, @USERNAME + "__subs" + from_id.to_s + ".dump")
    if from_id == 0
      begin
        File.delete(@SUBS_FILE)
      rescue
      end
    end

    if subs.size < @SUBS_LIMIT # limit 以下の場合に完了と解釈
      buf = []
      Dir.glob(@USERNAME + "__subs*.dump") do |item|
        buf.concat(MyUtil.load_obj(item))
        File.delete(item)
      end
      MyUtil.save_obj(buf,@SUBS_FILE)
      redirect(@SCR[:load_folder])
    else
      last = subs.delete_at( subs.size - 1)
      # もう一度リクエストするために、from_id を追加した同じ URL にリ
      redirect(@SCR[:load_subs], "from_id=" + last["subscribe_id"].to_s)
    end
    cgiout( headers ) {""}
  end

  def main_view
    h = [
         [url(@SCR[:folder])  ,  "フォルダ一覧"],
         [url(@SCR[:rate])  ,  "レート一覧"],
         [url(@SCR[:subscribers])  ,  "購読者数別"],
         [url(@SCR[:modified_on])  ,  "更新日別"],
         [url(@SCR[:reload_yaml])  ,  "データ再読み込み"],
         [url(@SCR[:history])  ,  "閲覧履歴"],
         [url(0)  ,  "ログインしなおす"]
        ]
    buf = ErbMan.new.result(:main,binding)
    cgiout(headers){buf}
  end

  def folder_list(page)
    page = page.to_i
    param = 'key=folder&type=value&value='
    page_count = (get_folders.size / @LINK_LIST_LIMIT)
    page_count = page_count + 1 if get_folders.size % @LINK_LIST_LIMIT > 0

    folders = get_folders
    folders[""] = ""
    folders = folders.sort.slice(page * @LINK_LIST_LIMIT, @LINK_LIST_LIMIT)

    buf = ErbMan.new.erb(:folder_list).result(binding)
    cgiout(headers){buf}
  end

  def rate_view
    param = 'key=rate&type=value&value='
    buf = ErbMan.new.erb(:rate_list).result(binding)
    cgiout(headers){buf}
  end

  def subscribers_view
    param = url(@SCR[:list], "key=subscribers_count&type=range&value=")
    c = 'subscribers_count'
    buf = ErbMan.new.erb(:subscribers_list).result(binding)
    cgiout(headers){buf}
  end

  def modfied_view
    param = url(@SCR[:list],"key=modified_on&type=range&value=")
    m = "modified_on"
    buf = ErbMan.new.erb(:modified_list).result(binding)
    cgiout(headers){buf}
  end

  # subs の中から指定されたキーに合致する unread をカウントする
  def count_unread(&b)
    count = 0
    get_subs.each do |item|
      count = count + item['unread_count'].to_i if yield(item)
    end
    count.to_s
  end

  # 引数 type により若干振舞が異なる
  # type = value : 引数 key のフィールドを value で検索する
  # type = range : 引数 key のフィールドを分割し value 番目を利用する
  def feed_list_view(value, key, type, skey, page=nil)#, id)
    # フォルダ名で subs を検索する為に folder id からフォルダ名を引いてくる
    if key == "folder"
      get_folders.each {|name, id| value = name if id == value }
    end

    ary = case type
      when "value"
        filter_value get_subs, value, key
      when "range"
        filter_range get_subs, value, key
      else
        raise
      end

    page_count = (ary.size / @LINK_LIST_LIMIT)
    page_count = page_count + 1 if ary.size % @LINK_LIST_LIMIT > 0

    if skey.size > 0
      if skey =~ /_rev$/
        reverse = true
        skey = String.new(skey)[0 .. (skey =~ /_rev$/) -1]
      end
      p = Proc.new {|a, b| a[skey] <=> b[skey] }
      ary = ary.sort do |a, b|
        if skey == "title" 
          a[skey] <=> b[skey]
        else
          b[skey].to_i <=> a[skey].to_i
        end
      end
      ary = ary.reverse if reverse
    end
    
    ary = fillter_page(ary, page, @LINK_LIST_LIMIT)
    buf = ErbMan.new.erb(:feed_list).result(binding)
    cgiout(headers) { buf  }
  end

  # subs の key を value で検索した群を返す
  def filter_value( subs, value, key )
    ret = []
    subs.each {|item| ret.push item if item[key].to_s == value.to_s}
    ret
  end

  # subs を limitの数でページ分けした群れを返す
  def fillter_page( subs, page, limit)
    subs.slice(page * limit, limit)
  end

  # subs を key でソートして、5 で割り、0 から 4 番目の群を返す
  def filter_range( subs, range, key )
    s_subs = subs.sort {|a, b| a[key].to_i <=> b[key].to_i }
    a = s_subs.slice((s_subs.length / 5) * range.to_i, (s_subs.length / 5))

    # 上では剰余がある場合に対応できないのでフォロー
    if ( range.to_i == 4 && s_subs.length / 5 > 0)
      tmp = s_subs.slice((s_subs.length / 5) * 5, (s_subs.length / 5))
      a + tmp
    end
    a.reverse
  end

  def replace_view_option(buf)
    # todo:この辺やっつけすぎなのでアレする。DOM とか使うべきなのかな？
    case @USE_LINK_REPLACE
    when "gmobile"
      buf.gsub!(/href="?([^" ]+)"?/) { 'href="http://www.google.co.jp/gwt/n?u=' + CGI.escape($1) + '"' }
    when "hatena"
      buf.gsub!(/href="?([^" ]+)"?/) { 'href="http://mgw.hatena.ne.jp/?url='+ CGI.escape($1) + '"' }
    end

    
    if @USE_IMENU
      buf.gsub!(/href="?http(s?):\/\/([^" ]+)"?/) { 'href="./imenu.cgi?s=' + $1 + '&u='+ CGI.escape($2) + '"' }
    end

    case @USE_INLINE_IMAGE_REPLACE
    when "mobapic"
      buf.gsub!(/<img(.*) src="?([^" ]+)"?/){ "<img" + $1 + "src=\"http://red-eyes.jp/api/mbpic/" + CGI.escape($2) + '"'}
    when "hatena"
      buf.gsub!(/<img(.*)src="?([^" ]+)"?/){ "<img" + $1 + "src=\"http://mgw.hatena.ne.jp/?url=" + CGI.escape($2) + '"'}
    end

    if @USE_INLINE_IMAGE_REPLACE2 == "noimage"
      buf.gsub!(/<img src=([^>]+)>/){ "<a href=" + $1 + ">image</a>" }
    end
    buf.gsub!(/_link/,"a href")
    return buf
  end

  def save_history(title,url)
    history = get_history
    history.unshift({ "url" => url, "title" => title })
    MyUtil.save_obj(history[0..100],@HISTORY_FILE)
  end

  def history_view()
    history = get_history
    buf = ""
    history.each{|i|
        buf << "<li><a href=\"" + i["url"] + "\">" + i["title"] +" </a>"
    }
    cgiout(headers){buf}
  end

  def feed_view(subscribe_id, offset=0)
    if @TITLE_MODE
      limit = @LINK_LIST_LIMIT
    else
      limit = @LIMIT
    end
    if offset == 0
      feed = @l.unread(subscribe_id)
      feed["items"] = feed["items"][0..limit] # 表示処理に行く前に、制限件数に切り落す
      if feed["items"].size == 0
        feed = @l.all(subscribe_id, offset, limit)
      end
    else 
      feed = @l.all(subscribe_id, offset, limit)
    end
    touch_all(subscribe_id) if @YONDARA_KIDOKU

    save_history( feed["channel"]["title"], feed["channel"]["link"] )
    c = feed["channel"]
    next_url = url(@SCR[:view],"subscribe_id=" + subscribe_id, "offset=" + (offset + feed['items'].size).to_s)
#    next_url = url(@SCR[:view],"subscribe_id=" + subscribe_id, "offset=" + (offset + limit).to_s)
    kidoku_url = url(@SCR[:touch_all], "subscribe_id=" + subscribe_id)
    hatebu_url = 'http://b.hatena.ne.jp/addmobile?mode=confirm&title=&url='
    buf = ErbMan.new.erb(:feed_view).result(binding)
    buf = replace_view_option buf

    cgiout(headers){buf}
  end

  def touch_all_view(subscribe_id)
    touch_all(subscribe_id)
    cgiout(headers) {
      "既読にしました(多分)。データリロードするまでは表示されます"
    }
  end

  def touch_all(subscribe_id)
    @l.touch_all(subscribe_id.to_i)
    subs = get_subs
    subs.each do |ary|
      if ary["subscribe_id"] === subscribe_id.to_i
        subs.delete ary
        MyUtil.save_obj(subs, @SUBS_FILE)
        break
      end
    end
  end

# ================================================================================
# main 関数ってやつですよ
  def run
    if @cgi.key?('username')
      @USERNAME = @cgi['username']
      @SUBS_FILE = @USERNAME + "__subs.dump"
      @FOLDERS_FILE = @USERNAME + "__folders.dump"
      @HISTORY_FILE = @USERNAME + "__history.dump"

      @l = LivedoorReaderWp.new(@USERNAME,@USE_GZIP)

      if @cgi['scr'].to_i == @SCR[:login]
        login_redirect @cgi['password']
        return
      end

      unless @l.logged?
        login("err:ログオンしなおし")
        return
      end

      case @cgi['scr'].to_i
      when @SCR[:folder]
        folder_list @cgi['page']
      when @SCR[:rate]
        rate_view
      when @SCR[:subscribers]
        subscribers_view
      when @SCR[:modified_on]
        modfied_view
      when @SCR[:history]
        history_view
      when @SCR[:list]
        feed_list_view @cgi["value"], @cgi["key"], @cgi["type"], @cgi["skey"], @cgi["page"].to_i
      when @SCR[:load_subs]
        load_subs @cgi["from_id"].to_i
      when @SCR[:load_folder]
        load_folder
      when @SCR[:main]
        main_view
      when @SCR[:view]
        feed_view(@cgi["subscribe_id"], @cgi["offset"].to_i)
      when @SCR[:reload_yaml]
        reload_yaml
      when @SCR[:touch_all]
        touch_all_view @cgi["subscribe_id"].to_i
      when 0
        login("")
      else
        login("画面ID不正")
      end
    else
      login("")
    end
  end

  def headers
    {
      "language" => "ja",
      "type" => "text/html",
      "charset" => "Shift_JIS"
    }
  end

  #todo:zlib で gzip する
  def cgiout(headers)
    buf =  NKF.nkf( '-Ws',  <<-EOF )
      <htmll><head><title>MobileReader</title></head><body>
      #{yield}
      </body></hteml>
    EOF

    if @USE_GZIP
      if ENV["HTTP_ACCEPT_ENCODING"]=~/gzip/
        require 'zlib'
        require 'stringio'
        s=StringIO.new
        Zlib::GzipWriter.wrap(s) do |gz|
          gz.write(buf)
          gz.finish
        end
        buf=s.string
        
        if ENV["HTTP_ACCEPT_ENCODING"]=~/x-gzip/
          headers["Content-encoding"]="x-gzip"
        else
          headers["Content-encoding"]="gzip"
        end
      end
    end

    headers["Content-length"]=buf.length
    print @cgi.header(headers)
    print buf
#    @cgi.out(headers){buf} # なんでこれ動かんの？
  end

end
m = MobileReader.new
m.run
